埋め込みのパラメータによるRouting – Ember.js入門(17)
渡辺です。
今回は埋め込みのパラメータによるRoutingについて解説します。前回 *1解説したFixtureAdapterを使い、ローカル環境で動作するブログアプリケーションを作成してみます。このアプリケーションは、全エントリー一覧と個別のエントリー表示の2画面で構成されます。
埋め込みパラメータ
しばしば、アプリケーションではなんらかのModelに対する一覧画面と詳細表示画面をセットで構築します。例えば、一般的なWebアプリケーションでは、全エントリーの一覧と個別のエントリーのURLは次のように設計できるでしょう。
http://hostname/entries http://hostname/entry/1 http://hostname/entry/2 http://hostname/entry/3
デフォルトでRouting機能を持つEmber.jsでは、このような埋め込みパラメータに対応しています。
Routingでパラメータを埋め込んだパスを設定し、Routeでは埋め込みパラメータを使ってModelを取得できます。そして、全エントリー画面ではArrayControllerでViewをハンドリングし、個別エントリー画面ではObjectControllerでViewをハンドリングします *2。
Routingの定義
埋め込みパラメータを設定したパスをRoutingに定義するのは簡単です。次のように:(コロン)付のパラメータ名をパスに設定します *3。
App.Routing.map(function() { this.route('entries', { path: '/' }); this.route('entry', { path: '/entry/:id' }); });
これで、/entry/1
や/entry/2
など /entry/
ではじまりパラメータをひとつ持つパスは、entry routeで処理されます。
なお、例えばフィルタ条件など、埋め込みパラメータではなく、クエリパラメータとして扱いたいパラメータもあります。Ember 1.2.0ではクエリパラメータには対応していませんが、1.4では対応される予定です *4。
Routeと埋め込みパラメータ
Routingで埋め込みパラメータがあるRouteが選択されたならば、パラメータを取得してmodelに反映しなければなりません。この時に利用するのがRouteのmodelメソッドです。
modelメソッドはparams
とtransition
のふたつのパラメータを引数として持ちます *5。このparamsオブジェクトに埋め込みパラメータが含まれます。なお、transitionオブジェクトはパラメータが不正である時など画面遷移(transition)を行いたい場合に利用するオブジェクトです。
したがって、EntriesRouteとEntryRouteは次のように定義することができます。
App.EntriesRoute = Ember.Route.extend({ model: function(params, transition) { return this.store.find('entry'); } }); App.EntryRoute = Ember.Route.extend({ model: function(params, transition) { return this.store.find('entry', params['id']); } });
EntriesRouteのmodelメソッドでは全てのEntryを含む配列を返しますが、EntryRouteのmodelメソッドでは埋め込みパラメータに含まれるidを指定して、単一のEntryを返していることに注意してください。
なお、今回のサンプルではComputed PropertyやViewの状態を扱わないので、EntriesControllerとEntryControllerは定義しなくても暗黙的に作られます。しかし、デフォルトのままで済むことは、まずありません。明示的に宣言することをオススメします。
App.EntriesController = Ember.ArrayController.extend({ }); App.EntryController = Ember.ObjectController.extend({ });
link-toヘルパーとパスパラメータ
link-toヘルパーはRoute名を指定し、ハイパーリンクを作成するViewヘルパーです。個別のエントリー画面から一覧画面に戻るリンクであれば次のように、Route名を指定するだけで充分です。
{{#link-to 'entries'}}戻る{{/link-to}}
一方、一覧画面から個別のエントリー画面へ遷移するリンクを作成する場合は、どのEntry(ID)を対象としているかを指定する必要があります。この指定方法としてはidを渡すかModelを渡すかのふたつの方法があります。
idを渡す
link-toヘルパーにModelオブジェクトのIDを指定します。
{{#each e in model}} {{#link-to 'entry' e.id}}{{e.title}}{{/link-to}} {{/each}}
第2引数に指定したEntryのidはRoutingで設定したパス(/entry/:id)の:id部分に埋め込まれてリンクが生成されます。なお、Ember.jsでは細かい部分をよしなにやってくれますので、:idは:entry_idでも構いません *6。ただし、Routeのmodelでパラメータを取得する場合の名前も変わりますので注意しましょう。
Modelを渡す
link-toヘルパーにModelオブジェクト自体を指定しても期待通りに動作します。
{{#each e in model}} {{#link-to 'entry' e}}{{e.title}}{{/link-to}} {{/each}}
公式のドキュメントを読む限りは、Modelを指定する方法を選択することが推奨されているように思われます。しかし、暗黙的にidが使われるなど解りにくく、複雑なパスパラメータを扱う場合などで細かい制御が難しいため、自分はidを明示的に指定する方法で統一した方が良いと考えています。
暗黙的なコンテキストを指定する場合
次のようにeachヘルパーは対象となるModelを指定しない反復処理を記述できます。
{{#each}} {{#link-to 'entry' ???}}{{title}}{{/link-to}} {{/each}}
この時、Modelはcontextという名前で渡す事ができます。
{{#each}} {{#link-to 'entry' context}}{{title}}{{/link-to}} {{/each}}
言い換えれば、Modelにcontextという属性を定義するとハマります。Ember.jsはよしなにやり過ぎていて、たまに変なところでハマるので注意しましょう。
まとめ
今回はember-dataを使って一覧と個別ページを作成する流れを解説しました。ポイントは、RoutingにおけるURLの埋め込みパラメータの定義と、Routeクラスのmodelメソッドでのパラメータの扱いです。また、Routingに埋め込みパラメータが設定されている場合は、link-toヘルパーの第2引数でパラメータを指定してください。このような一覧と個別表示はよくある画面構成になるため、確実におさえておきましょう。
最後にサンプルとなるソースコード全体を掲載します。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Blog Application Ember.js</title> </head> <body> <script type="text/x-handlebars" data-template-name="entries"> <h1>Entries</h1> <ul> {{#each e in model}} <li>{{#link-to 'entry' e.id}}{{e.title}}{{/link-to}} {{/each}} </ul> </script> <script type="text/x-handlebars" data-template-name="entry"> <h2>{{title}} - {{category.name}}</h2> <p>{{contentBody}}</p> <p>{{#link-to 'entries'}}[BACK]{{/link-to}} </script> <script type="text/javascript" src="../../ember/1.2.0/jquery-1.10.2.js"></script> <script type="text/javascript" src="../../ember/1.2.0/handlebars-v1.1.2.js"></script> <script type="text/javascript" src="../../ember/1.2.0/ember-1.2.0.js"></script> <script type="text/javascript" src="../../ember/1.2.0/ember-data-1.0.0-beta.4.js"></script> <script type="text/javascript"> // application.js window.App = Ember.Application.create(); // model.js App.Category = DS.Model.extend({ name: DS.attr('string'), entries: DS.hasMany('entry') }); App.Entry = DS.Model.extend({ title: DS.attr('string'), postedAt: DS.attr('date'), contentBody: DS.attr('string'), category: DS.belongsTo('category') }); App.Category.FIXTURES = [ { id: 1, name: 'お知らせ', entries: [1, 2] } ]; // model.fixtures.js App.ApplicationAdapter = DS.FixtureAdapter.extend(); App.Entry.FIXTURES = [ { id: 1, title: 'お知らせ1', contentBody: 'お知らせ1の本文', category: 1 }, { id: 2, title: 'お知らせ2', contentBody: 'お知らせ2の本文', category: 1 } ]; // application.js App.Router.map(function() { this.route("entries", { path: "/" }); this.route("entry", { path: "/entry/:id" }); }); // entries.js App.EntriesRoute = Ember.Route.extend({ model: function(params, transiton) { return this.store.find('entry'); } }); App.EntriesController = Ember.ArrayController.extend({ }); // entry.js App.EntryController = Ember.ObjectController.extend({ }); App.EntryRoute = Ember.Route.extend({ model: function(params, transiton) { return this.store.find('entry', params['id']); } }); </script> </body> </html>